/// Text Overlay Application: Timeline Class
/// This class contains all the functions necessary to create and operate a Timeline.
/// This class should be instanciated once per each channel.

using System;
using System.Collections.Generic;
using System.Collections;
using System.Text.RegularExpressions;
using System.Threading;
using System.Text;
using System.Xml;
using System.IO;

namespace TextOverlay
{
    class CTimeline
    {
        public delegate int CallbackFromMain(string Facename, int TheSize);
        public delegate int CallbackMeasure(string mystring, string facename, int size);
        public delegate int CallbackImageSize(int id);
        private CallbackFromMain FontIDCallBack;
        private CallbackMeasure MeasureStringCallBack;
        private CallbackImageSize MeasureImageCallBack;

        public List<Combo> DXComboLib = new List<Combo>();
        public ComboSchedule[] Schedule = new ComboSchedule[3];
        private CExpressions Expression = new CExpressions();
        public bool SwapMe = false;
        public int ActiveSchedule = 0;
        public int PasiveSchedule = 1;

        public int RTDiff;
       
        public int ChannelID = 0;
        public int ChannelFrame = 0;
        public int SyncFrame = 0;
        public int JumpFrom = 0;
        public int JumpTo = 0;
        public int UnPause = 0;
        public int State = 1;

        public int ScaleFactor = 1;
        private int lastCall = 0;

        public CTimeline(int Scale, CallbackFromMain aCallback, CallbackMeasure MeasureCallBack, CallbackImageSize MeasureImage)
        {
            FontIDCallBack = aCallback;
            MeasureStringCallBack = MeasureCallBack;
            MeasureImageCallBack = MeasureImage;
            Schedule[0] = new ComboSchedule(20000);
            Schedule[1] = new ComboSchedule(20000);
            Schedule[2] = new ComboSchedule(20000);
            ScaleFactor = Scale;
            Expression = new CExpressions(true);
        }

        public bool UpdateFromXML(XmlDataDocument XMLDocu)
        {
            try
            {
                // Select the channel node to extract channel info
                // Select all the <Combo> nodes and creates a new TimeLine
                // with that amount of combo objects
                lastCall = 2011;
                XmlNode Channel = XMLDocu.SelectSingleNode("//Channel");
                XmlNodeList Combolist = XMLDocu.SelectNodes("//Combo");
                if (Channel == null) return false;
                if (Combolist.Count < 1) return false;

                if (ChannelID == 2)
                    ChannelID = 2;

                int UpdateStartFrame = Expression.TimeToFrame(Channel.Attributes["StartTime"].InnerText, 60);
                int UpdateEndFrame = Expression.TimeToFrame(Channel.Attributes["EndTime"].InnerText, 60);

                if (UpdateStartFrame == 216000)
                    UpdateStartFrame = 216000;

                // Find which Combos should be replaced by the new update
                // all combos that intercept the update totally or patially in the 
                // timeline, will be deleted
                lastCall = 2012;
                int DeleteFrom = 0;
                int DeleteTo = 0;

                for (int df = 0; df < Schedule[0].SchCombo.Length; df++)
                {
                    if (Schedule[0].SchCombo[df].endFrame > UpdateStartFrame || Schedule[0].SchCombo[df].ComboIndex < 1)
                    {
                        DeleteFrom = df;
                        DeleteTo = df;
                        break;
                    }
                }

                for (int dt = DeleteFrom; dt < Schedule[0].SchCombo.Length; dt++)
                {
                    if (Schedule[0].SchCombo[dt].startFrame >= UpdateEndFrame || Schedule[0].SchCombo[dt].ComboIndex < 1)
                    {
                        DeleteTo = dt;
                        break;
                    }
                }
        

                // Clear The Passive Schedule
                for (int x = 0; x < Schedule[PasiveSchedule].SchCombo.Length; x++)
                {
                    Schedule[PasiveSchedule].SchCombo[x].ComboIndex = -1;
                    Schedule[PasiveSchedule].SchCombo[x].endFrame = 0;
                    Schedule[PasiveSchedule].SchCombo[x].endFrame = 0;
                }


                // Copy all Combos BEFORE the update
                lastCall = 2013;
                for (int x = 0; x < DeleteFrom; x++)
                {
                    Schedule[PasiveSchedule].SchCombo[x].ComboIndex = Schedule[0].SchCombo[x].ComboIndex;
                    Schedule[PasiveSchedule].SchCombo[x].startFrame = Schedule[0].SchCombo[x].startFrame;
                    Schedule[PasiveSchedule].SchCombo[x].endFrame = Schedule[0].SchCombo[x].endFrame;
                }

                int badcombos = 0;
                // Extract all Combos and check if they already exist in the Combo Library
                // if they exist, get a pointer to their location, if they dont exist, create
                // them and return a pointer to them
                lastCall = 2014;
                for (int i = 0; i < Combolist.Count; i++)
                {
                    if (Combolist[i].InnerText != "<TextString />" && Combolist[i].InnerText != "")
                    {
                        Schedule[PasiveSchedule].SchCombo[i + DeleteFrom].ComboIndex = LocateCombo(Combolist[i]);
                        Schedule[PasiveSchedule].SchCombo[i + DeleteFrom].startFrame = Expression.TimeToFrame(Combolist[i].Attributes["TimelineTime"].InnerText, 60);
                        Schedule[PasiveSchedule].SchCombo[i + DeleteFrom].endFrame = Schedule[PasiveSchedule].SchCombo[i + DeleteFrom].startFrame + Expression.TimeToFrame(Combolist[i].Attributes["Duration"].InnerText, 60) - 1;
                    }
                    else
                    {
                        badcombos++;
                    }
                }

                // Copy all Combos AFTER the update
                lastCall = 2015;
                for (int x = 0; x < 19999; x++)
                {
                    int Destination = DeleteFrom + (Combolist.Count - badcombos);
                    int Origin = DeleteTo; // aqui era DeleteTo + 1;
                   
                    if ((x + Destination) < 20000 && (x + Origin) < 20000)
                    {
                        Schedule[PasiveSchedule].SchCombo[x + Destination].ComboIndex = Schedule[0].SchCombo[x + Origin].ComboIndex;
                        Schedule[PasiveSchedule].SchCombo[x + Destination].startFrame = Schedule[0].SchCombo[x + Origin].startFrame;
                        Schedule[PasiveSchedule].SchCombo[x + Destination].endFrame = Schedule[0].SchCombo[x + Origin].endFrame; 
                    }
                }

                // Clear the Schedule[0]
                // Copy from Schedule[0] to the Passive Schedule
                for (int x = 0; x < Schedule[0].SchCombo.Length; x++)
                {
                    Schedule[0].SchCombo[x].ComboIndex = -1;
                    Schedule[0].SchCombo[x].endFrame = 0;
                    Schedule[0].SchCombo[x].endFrame = 0;
                }

                lastCall = 2016;
                for (int x = 0; x < 20000; x++)
                {
                    Schedule[0].SchCombo[x].ComboIndex = Schedule[PasiveSchedule].SchCombo[x].ComboIndex;
                    Schedule[0].SchCombo[x].startFrame = Schedule[PasiveSchedule].SchCombo[x].startFrame;
                    Schedule[0].SchCombo[x].endFrame = Schedule[PasiveSchedule].SchCombo[x].endFrame;
                }
                return true;
            }
            catch
            {
                Track.LogInfo("Error Loading XML Update (code " + lastCall.ToString() + ")");
                return false;
            }

        }


        /// <summary>
        /// This function attempts to locate a Combo in the Combo Library. if the combo is not located
        /// the CreateCombo function is called to create a new combo using the node information.
        /// </summary>
        /// <param name="ComboNode">Xml Node containing the combo information</param>
        /// <returns>the id of the combo in DXComboLib</returns>
        public int LocateCombo(XmlNode ComboNode)
        {
            try
            {
                lastCall = 2019;
                // check if the combo exist in the library
                int NewHash = ComboNode.InnerText.GetHashCode();
                for (int x = 0; x < DXComboLib.Count; x++)
                {

                    if (NewHash == DXComboLib[x].HashID)
                    {
                        //return x + 1;
                        if (ComboNode.InnerText == DXComboLib[x].FullXml)
                            return x + 1;
                    }

                }

                // if the combo does not exist in the library, create it
                if (CreateCombo(ComboNode) == true)
                    return (DXComboLib.Count);

                return -1;
            }
            catch
            {
                Track.LogInfo("Error Updating Timeline (LocateCombo)");
                return 0;
            }

        }

        private ArrayList myArray = new ArrayList();
        /// <summary>
        /// This function is called by the LocateCombo function when a combo can not be
        /// located in the ComboLibrary. This will create a new combo in the DXComboLib.
        /// </summary>
        /// <param name="node">Xml Node containing the Combo information</param>
        /// <returns>true if the combo was created succesfully</returns>
        public bool CreateCombo(XmlNode node)
        {
            try
            {
                myArray.Clear();

                // Extract all elements from the Combo
                // Counting each string and dividing it into
                // as many substrings as necesary
                lastCall = 2020;
                XmlNodeList list = node.SelectNodes("TextString");
                lastCall = 2021;
                foreach (XmlNode item in list)
                {
                    // Get all the substring in the combo
                    if (item.InnerText != "")
                        Get_Substrings(myArray, item["String"].InnerText, item["Details"].InnerText.ToUpper());
                }

                if (myArray.Count < 1)
                    return false;

                // Initialize the Combo Class
                lastCall = 2022;
                Combo TempCombo = new Combo(node.InnerText, myArray.Count / 3, ScaleFactor, node.Attributes["ComboID"].InnerText, node.Attributes["ComboName"].InnerText,
                     node.Attributes["TimelineTime"].InnerText, node.Attributes["Duration"].InnerText);

                lastCall = 2023;
                string TempFullString = node.InnerText;
                TempCombo.HashID = TempFullString.GetHashCode();
                TempCombo.StringLength = TempFullString.Length;
                TempCombo.CalculateFrames();
                TempCombo.CreateElementsArray(myArray.Count / 3);

                // Create the New Element, then update its position if it is a substring 
                // extracted from a larger string and then check if its font needs to be
                // created.
                lastCall = 2024;
                int i, h;
                for (i = 0; i < myArray.Count; i += 3)
                {
                    TempCombo.ParseElement(i / 3, (string)myArray[i], (string)myArray[i + 1], Expression);
                    TempCombo.Fragment[i / 3].startframe += TempCombo.StartFrame;
                    TempCombo.Fragment[i / 3].endframe += TempCombo.StartFrame;
                    TempCombo.Fragment[i / 3].FontID = FontIDCallBack(TempCombo.Fragment[i / 3].FontFace, TempCombo.Fragment[i / 3].FontSize);
                    TempCombo.Fragment[i / 3].hierarchy = Convert.ToInt32(myArray[i + 2]);
                }

                lastCall = 2025;
                for (i = 0; i < TempCombo.Fragment.Length; i++)
                {
                    TempCombo.Fragment[i].stringwidth = MeasureStringCallBack(TempCombo.Fragment[i].m_string, TempCombo.Fragment[i].FontFace, TempCombo.Fragment[i].FontSize);
                    // Exceptions 
                    if (TempCombo.Fragment[i].m_string.ToUpper() == "[B]") TempCombo.Fragment[i].stringwidth = 0;
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[HL]") TempCombo.Fragment[i].stringwidth = 0;
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[HR]") TempCombo.Fragment[i].stringwidth = 0;
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[HT]") TempCombo.Fragment[i].stringwidth = 0;
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[HB]") TempCombo.Fragment[i].stringwidth = 0;
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[HM]") TempCombo.Fragment[i].stringwidth = 0;
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[DATATAG LIVETIME24HM]") TempCombo.Fragment[i].stringwidth = MeasureStringCallBack(".00:00.", TempCombo.Fragment[i].FontFace, TempCombo.Fragment[i].FontSize);
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[DATATAG LIVETIME24HMS]") TempCombo.Fragment[i].stringwidth = MeasureStringCallBack(".00:00:00.", TempCombo.Fragment[i].FontFace, TempCombo.Fragment[i].FontSize);
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[DATATAG LIVETIMEAPHM]") TempCombo.Fragment[i].stringwidth = MeasureStringCallBack(".00:00 PM.", TempCombo.Fragment[i].FontFace, TempCombo.Fragment[i].FontSize);
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[DATATAG LIVETIMEAPHMS]") TempCombo.Fragment[i].stringwidth = MeasureStringCallBack(".00:00:00 PM.", TempCombo.Fragment[i].FontFace, TempCombo.Fragment[i].FontSize);
                    else if (TempCombo.Fragment[i].m_string.ToUpper() == "[COUNTDOWN MIDNIGHT]") TempCombo.Fragment[i].stringwidth = MeasureStringCallBack(".00:00:00.", TempCombo.Fragment[i].FontFace, TempCombo.Fragment[i].FontSize);
                    else if (TempCombo.Fragment[i].m_string.Contains("[IMAGETAG ID")) TempCombo.Fragment[i].stringwidth = MeasureImageCallBack(Expression.GetIntValue(TempCombo.Fragment[i].m_string));
                    for (h = 1; h <= TempCombo.Fragment[i].hierarchy; h++)
                    {
                        TempCombo.Fragment[i].startx += TempCombo.Fragment[i - h].stringwidth;
                        TempCombo.Fragment[i].endx += TempCombo.Fragment[i - h].stringwidth;
                    }
                }

                lastCall = 2026;
                // Add Combo to Timeline Arraylist of Combos
                DXComboLib.Add(TempCombo);
                CalculateTargetSpeed(DXComboLib.Count - 1);
                CalculateHoldPoint(DXComboLib.Count - 1);
                Track.LogInfo("New Combo Created " + DXComboLib.Count.ToString());
                list = null;
                TempFullString = null;
                return true;
            }
            catch
            {
                return false;
            }
        }

        private List<string> words = new List<string>();
        private List<string> subwords = new List<string>();
        private StringBuilder Temp = new StringBuilder();
        /// <summary>
        /// This function breaks a text string into pieces of 10 or 5 characters long each leaving
        /// appart all font, color and data tags
        /// </summary>
        /// <param name="Arr">Array list where all fragments will be stored</param>
        /// <param name="LongString">Text string to break into fragments</param>
        /// <param name="Details">Details string linked to the text string</param>
        /// <returns>true if succesfull</returns>
        public bool Get_Substrings(ArrayList Arr, string LongString, String Details)
        {
            try
            {
                int counter = 0;
                int hierarchy = 0;
                bool flag = false;

                if (LongString.Length < 1)
                    return false;

                // Make sure that the String ends with a break tag
                if (LongString.EndsWith("[b]") != true)
                    LongString = LongString + "[b]";


                // Prepare String to isolate fragments of text from all other meta tags
                LongString = LongString.Replace(" ", "~");
                LongString = LongString.Replace("[", " [");
                LongString = LongString.Replace("]", "] ");
                LongString = CleanString(LongString);

                if (LongString.ToUpper().Contains("[LOOP]") || LongString.ToUpper().Contains("[loop]") || LongString.ToUpper().Contains("[Loop]"))
                {
                    LongString = LoopString(LongString, Details);
                    LongString = LongString.Replace("[LOOP]", "");
                    LongString = LongString.Replace("[loop]", "");
                    LongString = LongString.Replace("[Loop]", "");
                }

                if (LongString.ToUpper().Contains("[CAPS]"))
                {
                    LongString = StringToUpper(LongString);
                }

                lastCall = 2034;
                Expression.SliptString(LongString, words);
                lastCall = 2035;
                for (int x = 0; x < words.Count; x++)
                {
                    if (Expression.GetDataTag("[COLOR~VAL=\"", words[x].ToUpper()) != "")
                    {
                        Details = Expression.ReplaceTag(Details, "COLOR=\"", "COLOR=\"" + Expression.answer + "\"");
                        flag = true;
                    }

                    if (Expression.GetFontTag(words[x]) != "")
                    {
                        Details = Expression.ReplaceTag(Details, "FONT=\"", "FONT=\"" + Expression.answer.ToUpper() + "\"");

                        flag = true;
                    }

                    if (Expression.GetBgColorTag(words[x]) != "")
                    {
                        Details = Details + ", BGKEY=\"" + Expression.answer + "\"";
                        flag = true;
                    }

                    if (Expression.GetBgImgTag(words[x].ToUpper()) != "")
                    {
                        Details = Details + ", BGIMAGE=\"" + Expression.answer + "\"";
                        flag = true;
                    }

                    if (words[x].ToUpper().Contains("[EL"))
                    {
                        flag = true;
                    }

                    if (words[x].ToUpper().Contains("[H"))
                    {
                        flag = false;
                    }

                    if (words[x].ToUpper().Contains("[B]"))
                    {
                        // Save the break point as an element of width 0
                        // the width 0 will be set by the arrange string function
                        flag = false;
                    }

                    if (words[x].ToUpper().Contains("[IMAGETAG"))
                    {
                        // If the word contains a Image Tag, add it to the Arraylist as an
                        // independent element, increase the hierarchy counter and set flag to true so
                        // the element does not get processed anywhere else.
                        Arr.Add(words[x].Replace("~", " "));
                        Arr.Add(Details);
                        Arr.Add(hierarchy);
                        hierarchy++;
                        flag = true;
                    }

                    if (words[x].ToUpper().Contains("[DATATAG"))
                    {
                        // If the word contains a Close Caption Tag, add it to the Arraylist as an
                        // independent element, increase the hierarchy counter and set flag to true so
                        // the element does not get processed anywhere else.
                        Arr.Add(words[x].Replace("~", " "));
                        Arr.Add(Details);
                        Arr.Add(hierarchy);
                        hierarchy++;
                        flag = true;
                    }

                    if (words[x].ToUpper().Contains("[COUNTDOWN"))
                    {
                        // If the word contains a Image Tag, add it to the Arraylist as an
                        // independent element, increase the hierarchy counter and set flag to true so
                        // the element does not get processed anywhere else.
                        Arr.Add(words[x].Replace("~", " "));
                        Arr.Add(Details);
                        Arr.Add(hierarchy);
                        hierarchy++;
                        flag = true;
                    }

                    if (words[x].ToUpper().Contains("[CC"))
                    {
                        // If the word contains a Close Caption Tag, add it to the Arraylist as an
                        // independent element, increase the hierarchy counter and set flag to true so
                        // the element does not get processed anywhere else.
                        Arr.Add(words[x]);
                        Arr.Add(Details);
                        Arr.Add(0);
                        hierarchy++;
                        flag = true;
                    }


                    if (flag != true && words[x].Length > 0)
                    {
                        Expression.BreakString(words[x].Replace("~", " "), subwords, 10);
                        for (counter = 0; counter < subwords.Count; counter++)
                        {
                            Arr.Add(subwords[counter]);
                            Arr.Add(Details);
                            Arr.Add(hierarchy);
                            hierarchy++;
                        }
                        counter = 0;
                    }
                    flag = false;
                }
                return true;
            }
            catch
            {
                Track.LogInfo("Error Processing Substrings (Exception)");
                return false;
            }
        }

        /// <summary>
        /// This function is called when a [LOOP] tag is found in a TextString
        /// This function returns a new string that contains the original text string
        /// repeated as many times as necessary to ensure that there is enough text to cover
        /// all frames given the text's constant or target speed.
        /// </summary>
        /// <param name="LongString">The original text string to loop</param>
        /// <param name="Details">The details string linked to the text</param>
        /// <returns>a new text string containing the original string looped</returns>
        private string LoopString(string LongString, String Details)
        {
            try
            {
                LongString = LongString.Replace("[LOOP]", "");
                LongString = LongString.Replace("[loop]", "");
                LongString = LongString.Replace("[Loop]", "");
                int tspeed = 0;
                int sframe = Expression.GetIntValue(Expression.GetDataTag("STARTFRAME=\"", Details));
                int eframe = Expression.GetIntValue(Expression.GetDataTag("ENDFRAME=\"", Details));
                int tlength = 0;
                int slength = 0;
                string FontTag = " [FONT~VAL=\"Arial\"] ";
                string ColorTag = " [COLOR~VAL=\"White\"] ";
                string BgColorTag = "";
                string Original = LongString;
                string step;


                // Get Target Speed
                Expression.GetDataTag("TARGETSPEED=\"", Details);
                if (Expression.answer != "")
                    tspeed = Expression.GetIntValue(Expression.answer);

                // if we found no target speed, try finding constant speed
                if (tspeed == 0)
                {
                    Expression.GetDataTag("CONSTANTSPEED=\"", Details);
                    if (Expression.answer != "")
                        tspeed = Expression.GetIntValue(Expression.answer);
                }

                // if we found no target speed and no constant speed, exit the function and
                // log and event warning that a loop tag could not be processed due to the
                // abscense of Constant or Target speed.
                if (tspeed == 0)
                {
                    Expression.GetDataTag("STARTX=\"", Details);
                    int StartX = Convert.ToInt32(Expression.answer);
                    Expression.GetDataTag("ENDX=\"", Details);
                    int EndX = Convert.ToInt32(Expression.answer);
                    Expression.GetDataTag("STARTFRAME=\"", Details);
                    int StartFrame = Convert.ToInt32(Expression.answer);
                    Expression.GetDataTag("ENDFRAME=\"", Details);
                    int EndFrame = Convert.ToInt32(Expression.answer);
                    int Frames = EndFrame - StartFrame;
                    tspeed = (EndX - StartX) / Frames;
                }

                if (tspeed == 0)
                {
                    Track.LogInfo("Error Looping String: " + LongString);
                    return LongString;
                }

                tspeed = tspeed * ScaleFactor;

                // Get The Initial Font and Size
                Expression.GetDataTag("[FONT=\"", Details);
                if (Expression.answer != "")
                    FontTag = " [FONT~VAL=\"" + Expression.answer + "\"] ";
                Expression.GetDataTag("[COLOR=\"", Details);
                if (Expression.answer != "")
                    ColorTag = " [COLOR~VAL=\"" + Expression.answer + "\"] ";
                Expression.GetDataTag("[BGCOLOR~VAL=\"", LongString);
                if (Expression.answer != "")
                    BgColorTag = Expression.answer;

                // Remove Tags
                StringBuilder thisbuilder = new StringBuilder();
                Expression.SliptString(LongString, words);
                for (int x = 0; x < words.Count; x++)
                {
                    if (words[x].StartsWith("[") && words[x].EndsWith("]") && words[x] != "[b]" && words[x].Contains("IMAGE") != true)
                    {
                        // skip word
                    }
                    else
                    {
                        if (words[x] == "[b]" || words[x] == "[B]")
                            thisbuilder.Append(" " + words[x] + " ");
                        else
                            thisbuilder.Append(words[x]);
                    }
                }

                LongString = thisbuilder.ToString();

                // Calculate TargetSpeed and TargetLength
                tlength = Math.Abs(tspeed * (eframe - sframe));
                slength = MeasureStringCallBack(LongString.Replace("~", " "), "ARIAL", 100);
                int multipler = (tlength / slength);
                // Loop the String

                step = FontTag + ColorTag + Original;
                thisbuilder.Length = 0;
                if (BgColorTag != "")
                    thisbuilder.Append("[BGCOLOR~VAL=\"" + BgColorTag + "\"]");
                for (int x = 0; x < multipler; x++)
                {
                    thisbuilder.Append(step);
                }
                thisbuilder.Append(step);
                return thisbuilder.ToString();
            }
            catch
            {
                Track.LogInfo("Unable To loop String \"" + LongString + "\"");
                return LongString;
            }
        }

        /// <summary>
        /// This function transforms a text string into a UpperCase version of itself
        /// </summary>
        /// <param name="longString">The target string</param>
        /// <returns>An upper case version of the target string</returns>
        private string StringToUpper(string longString)
        {
            try
            {
                int start = longString.IndexOf("[CAPS]");
                if (start < 0)
                    start = longString.IndexOf("[caps]");
                if (start < 0)
                    start = longString.IndexOf("[Caps]");
                if (start < 0)
                    return longString;

                if (start > 0)
                    longString = longString.Substring(0, start) + longString.Substring(start + 6).ToUpper();
                else
                    longString = longString.ToUpper();

                return longString.Replace("[CAPS]", "");
            }
            catch
            {
                Track.LogInfo("Unable To UpperCase String \"" + longString + "\"");
                return longString;
            }
        }


        /// <summary>
        /// This function calculates the most adecuated target speed to ensure that the text string
        /// will end  at a break point (matching the target last pixel) at its final frame.
        /// This function uses the desired target speed specified for a combo and replaces it for the
        /// recalculated target speed, making all fragments of text after the selected break point invisible.
        /// </summary>
        /// <param name="cmb">The target combo</param>
        public void CalculateTargetSpeed(int cmb)
        {
            try
            {
                float targetspeed = 0.0f;
                float bestspeed = 0.0f;
                float thisspeed = 0.0f;
                float pixels = 0;
                float frames = 0;
                int child = 0;
                int OffPixels = 0;
                int OffFrames = 0;
                int TargetPixel = DXComboLib[cmb].Fragment[0].TargetPixel;

                for (int a = 0; a < DXComboLib[cmb].Fragment.Length; a++)
                {
                    if (DXComboLib[cmb].Fragment[a].TargetSpeed == 0)
                    {
                        DXComboLib[cmb].Fragment[a].Visible = true;
                        continue;
                    }
                    DXComboLib[cmb].Fragment[a].Visible = false;

                    if (DXComboLib[cmb].Fragment[a].hierarchy == 0)
                    {
                        bestspeed = 0.0f;
                        targetspeed = DXComboLib[cmb].Fragment[a].TargetSpeed;
                    }

                    if (DXComboLib[cmb].Fragment[a].m_string == "[b]" || DXComboLib[cmb].Fragment[a].m_string == "[B]")
                    {
                        OffPixels = 0;
                        OffFrames = 0;
                        if (DXComboLib[cmb].Fragment[a].AccelerateIn != null)
                            OffPixels += Expression.GetIntValue(DXComboLib[cmb].Fragment[a].AccelerateIn);
                        if (DXComboLib[cmb].Fragment[a].AccelerateOut != null)
                            OffPixels += Expression.GetIntValue(DXComboLib[cmb].Fragment[a].AccelerateOut);
                        if (DXComboLib[cmb].Fragment[a].SweepIn != null)
                            OffPixels += Expression.GetIntValue(DXComboLib[cmb].Fragment[a].SweepIn);
                        if (DXComboLib[cmb].Fragment[a].SweepOut != null)
                            OffPixels += Expression.GetIntValue(DXComboLib[cmb].Fragment[a].SweepOut);
                        if (DXComboLib[cmb].Fragment[a].SweepIn != null)
                            OffFrames += Expression.GetIntValue(DXComboLib[cmb].Fragment[a].SweepIn, 1);
                        if (DXComboLib[cmb].Fragment[a].SweepOut != null)
                            OffFrames += Expression.GetIntValue(DXComboLib[cmb].Fragment[a].SweepOut, 1);

                        frames = (DXComboLib[cmb].Fragment[a].endframe - DXComboLib[cmb].Fragment[a].startframe) - OffFrames;
                        pixels = (TargetPixel - DXComboLib[cmb].Fragment[a].startx) - (OffPixels * ScaleFactor);

                        thisspeed = pixels / frames;

                        if (Expression.Difference(targetspeed, thisspeed) <= Expression.Difference(targetspeed, bestspeed) || bestspeed == 0)
                        {
                            bestspeed = thisspeed;
                            for (child = 0; child <= DXComboLib[cmb].Fragment[a].hierarchy; child++)
                            {
                                DXComboLib[cmb].Fragment[a - child].Visible = true;
                            }
                        }
                        for (child = 0; child <= DXComboLib[cmb].Fragment[a].hierarchy; child++)
                            DXComboLib[cmb].Fragment[a - child].TargetSpeed = bestspeed;
                    }
                }
            }
            catch
            {
                Track.LogInfo("Error Processing Combo (Target Speed)");
            }

        }


        /// <summary>
        /// This function calculates the point where each fragment of text shold stop scrolling if
        /// the combo contains a holding point Tag.
        /// </summary>
        /// <param name="cmb">The target combo</param>
        public void CalculateHoldPoint(int cmb)
        {
            try
            {

                float MySpeed = 0.0f;
                for (int x = 0; x < DXComboLib[cmb].Fragment.Length; x++)
                {
                    if (DXComboLib[cmb].Fragment[x].hierarchy == 0 && DXComboLib[cmb].Fragment[x].HoldingFrame == null)
                        DXComboLib[cmb].Fragment[x].HoldingFrame = new ArrayList();

                    if (Math.Abs(DXComboLib[cmb].Fragment[x].ConstantSpeedX) > 0.0f)
                        MySpeed = Math.Abs(DXComboLib[cmb].Fragment[x].ConstantSpeedX);
                    if (Math.Abs(DXComboLib[cmb].Fragment[x].TargetSpeed) > 0.0f)
                        MySpeed = Math.Abs(DXComboLib[cmb].Fragment[x].TargetSpeed);




                    if (DXComboLib[cmb].Fragment[x].m_string.ToUpper() == "[HL]")
                    {
                        DXComboLib[cmb].Fragment[x - DXComboLib[cmb].Fragment[x].hierarchy].HoldingFrame.Add(Convert.ToInt32(DXComboLib[cmb].Fragment[x].startx / MySpeed));
                        continue;
                    }

                    if (DXComboLib[cmb].Fragment[x].m_string.ToUpper() == "[HM]")
                    {
                        DXComboLib[cmb].Fragment[x - DXComboLib[cmb].Fragment[x].hierarchy].HoldingFrame.Add(Convert.ToInt32((DXComboLib[cmb].Fragment[x].startx - (2048 * ScaleFactor)) / MySpeed));
                        continue;
                    }

                    // TODO: Confirm these values
                    if (DXComboLib[cmb].Fragment[x].m_string.ToUpper() == "[HR]")
                    {
                        DXComboLib[cmb].Fragment[x - DXComboLib[cmb].Fragment[x].hierarchy].HoldingFrame.Add(Convert.ToInt32((DXComboLib[cmb].Fragment[x].startx - (3800 * ScaleFactor)) / MySpeed));
                        continue;
                    }
                }

                for (int x = 0; x < DXComboLib[cmb].Fragment.Length; x++)
                {
                    if (DXComboLib[cmb].Fragment[x].HoldingFrame != null)
                        if (DXComboLib[cmb].Fragment[x].hierarchy == 0 && DXComboLib[cmb].Fragment[x].HoldingFrame.Count > 0)
                            DXComboLib[cmb].Fragment[x].HoldingFrame.Sort(new MyComparer());
                }
                return;
            }
            catch
            {
                Track.LogInfo("Error Calculating Holding Point in Combo: " + cmb.ToString());
            }
        }

        public int thisframe = 216000;
        public DateTime lastSync;
        public int FrameOffset;
        public int TotalOffset;
        public int FrameSync;
        public int SCSFrame;
        public TimeSpan SyncDiff;
        public DateTime SyncTime = DateTime.Now;
        public bool SyncMe = true;
        private bool isFirstSync = true;
        public bool SyncSCS = false;

        /// <summary>
        /// This function processes the synchronization signal received from the ShowControl system
        /// </summary>
        /// <param name="Now">The current Time</param>
        /// <returns>true if there was a sync was processed</returns>
        public bool Sync(DateTime Now)
        {
            if (State == 1 || isFirstSync == true)
            {
                if (SyncMe == true)
                {
                    SyncDiff = Now - SyncTime;

                    FrameSync = 0;
                    // Calculate the expected next frame
                    SCSFrame = SyncFrame + (int)((SyncDiff.TotalMilliseconds+8.0) / 16.667);
                    FrameOffset = (1 - (SCSFrame - thisframe));

                    if (FrameOffset != 0)
                        TotalOffset += FrameOffset;
                    else
                        TotalOffset = 0;

                    thisframe++;

                    if (Math.Abs(FrameOffset) > 5)
                        thisframe = SCSFrame;
                    else
                        if (TotalOffset > 5 || TotalOffset < -5)
                            SyncWithSCS();
                    
                    if (thisframe == JumpFrom)
                        thisframe = JumpTo;
                    lastSync = Now;
                    SyncMe = false;
                    return true;
                }
                else
                {
                    thisframe++;
                    if (thisframe == JumpFrom)
                        thisframe = JumpTo;
                    return false;
                }
            }
            thisframe = SyncFrame;
            return true;
        }

        public int GetFrame()
        {
            return thisframe;
        }

        public int ActiveCombo = 0;
        private int LastActiveCombo = 0;
        /// <summary>
        /// This function returns the index number of the Combo that its active in this timeline
        /// during a given frame. If the function finds that there is not active combo at the time,
        /// the function will return -1. To improve speed, the function will also store the index 
        /// number of the last active combo and start looking from that index on the next time this 
        /// function is called.
        /// </summary>
        /// <param name="frame">the current frame</param>
        /// <returns>the index of the active combo</returns>
        public int GetActiveCombo(int frame)
        {
            try
            {
                if (Schedule[ActiveSchedule].SchCombo == null)
                {
                    SyncWithSCS();
                    return -1;
                }

                for (int x = LastActiveCombo; x < Schedule[ActiveSchedule].SchCombo.Length; x++)
                {
                    if (Schedule[ActiveSchedule].SchCombo[x].startFrame <= frame
                        && Schedule[ActiveSchedule].SchCombo[x].endFrame >= frame)
                    {
                        LastActiveCombo = x;
                        return x;
                    }
                }

                for (int x = 0; x < LastActiveCombo; x++)
                {
                    if (Schedule[ActiveSchedule].SchCombo[x].startFrame <= frame
                        && Schedule[ActiveSchedule].SchCombo[x].endFrame >= frame)
                    {
                        LastActiveCombo = x;
                        return x;
                    }
                }

                SyncWithSCS();
                return -1;
            }
            catch
            {
                Track.LogInfo("Error in Function GetActiveCombo()");
                return -1;
            }
        }

        /// <summary>
        /// This function is called by the Sync() function after 5 consecutive syncrhonization 
        /// intervals (5 * 200 milliseconds aprox) had indicated that the application was out of sync.
        /// This function synchronizes the application current frame with the frame ShowControl system.
        /// </summary>
        public void SyncWithSCS()
        {
            if (TotalOffset > 5 || TotalOffset < -5)
            {
                thisframe = SCSFrame;
                TotalOffset = 0;
                isFirstSync = false;
                SyncSCS = true;
            }
        }


        /// <summary>
        /// This function switches between the passive and active schedules of the timeline.
        /// Schedule[0] is the dummy timeline and the application should never read from it to render
        /// Schedule[Active] is the timeline from where the application is reading what to render
        /// Schedule[Passive] is the timeline awaiting to be updated
        /// </summary>
        public void SwapCollection()
        {
            if (ActiveSchedule == 1)
            {
                ActiveSchedule = 2;
                PasiveSchedule = 1;
            }
            else
            {
                ActiveSchedule = 1;
                PasiveSchedule = 2;
            }
        }



        private StringBuilder builder = new StringBuilder();
        /// <summary>
        /// This function cleans a target string from invalid xml characters.
        /// </summary>
        /// <param name="incoming">the target string</param>
        /// <returns>the target string with all invalid xml characters removed</returns>
        public string CleanString(string incoming)
        {
            builder.Length = 0;
            char[] CharArray = incoming.ToCharArray();
            for (int x = 0; x < CharArray.Length; x++)
            {
                if (Convert.ToInt32(CharArray[x]) > 31)
                    builder.Append(CharArray[x].ToString());
                else
                    builder.Append(" ");
            }
            return builder.ToString();

        }

        public class MyComparer : IComparer
        {

            public int Compare(object aobj, object bobj)
            {

                int a = (int)aobj;

                int b = (int)bobj;

                return a > b ? 1 : (a == b ? 0 : -1);

            }

        }

    }

}
